Skip to content

[WC-3537]: Fix resizing issue with Signature pad - #2375

Open
r0b1n wants to merge 10 commits into
mainfrom
fix/signature
Open

[WC-3537]: Fix resizing issue with Signature pad#2375
r0b1n wants to merge 10 commits into
mainfrom
fix/signature

Conversation

@r0b1n

@r0b1n r0b1n commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Bug fixes

Strokes stop registering after widget resize

When the widget container was resized mid-stroke (e.g., opening browser DevTools), signature_pad left its internal _drawingStroke flag as true permanently. The pointerup listener had been removed from the window, so strokeEnd never fired and every subsequent pointerdown was silently dropped. Fixed by calling pad.off() before resizing the canvas and pad.on() after — this resets _drawingStroke and re-registers the pointer listeners.

Canvas initializes at wrong size

The canvas was initialized at the HTML default size (300×150) when the ResizeObserver fired before imageSource became available. On init, the pad now reads the container's actual dimensions directly before instantiating SignaturePad.

Code quality improvements

  • useSignaturePad owns the ResizeObserver — the hook now calls useResizeObserver internally and returns a containerRef alongside canvasRef. SizeContainer no longer manages the observer or exposes an onResize callback; it is a plain forwardRef component. This removes the prop-threading indirection, and the parentElement lookup.
  • pad.redraw() replaces manual toData()/clear()/fromData() — the library's own method handles the snapshot internally.
  • Pen options moved to a lookup table — replaced a useMemo with if/else chains with a Record<PenTypeEnum, Options> constant and a getPenOptions helper; type safety is enforced at the call site.
  • isSignatureInitialized ref removed — redundant alongside the existing signaturePadRef === null guard.
  • Clear-on-reset condition simplified — collapsed three nested if blocks into a single expression.

@r0b1n
r0b1n requested a review from a team as a code owner August 7, 2026 14:06
@github-actions

This comment has been minimized.

@github-actions

Copy link
Copy Markdown
Contributor

AI Code Review

🔶 Changes requested — one or more medium-severity items must be addressed


What was reviewed

File Change
packages/pluggableWidgets/signature-web/src/utils/useSignaturePad.ts Core hook refactor: ResizeObserver ownership moved in, pad.off()/on() fix, pen-options lookup table, isSignatureInitialized removed
packages/pluggableWidgets/signature-web/src/components/SizeContainer.tsx Converted from FC to forwardRef; classNameInner/onResize props removed
packages/pluggableWidgets/signature-web/src/components/Signature.tsx Consumes containerRef instead of onResize; passes ref to SizeContainer
packages/pluggableWidgets/signature-web/src/Signature.editorPreview.tsx Dropped classNameInner call-site now that it's hardcoded in SizeContainer
packages/pluggableWidgets/signature-web/src/__tests__/useSignaturePad.spec.tsx New test file covering canvas initialization
packages/pluggableWidgets/signature-web/CHANGELOG.md Two Fixed entries added under [Unreleased]

Skipped (out of scope): dist/, pnpm-lock.yaml

CI check status: command required approval — could not be verified automatically.


Findings

🔶 Medium — Primary bug fix (resize handler) has no test coverage

File: packages/pluggableWidgets/signature-web/src/__tests__/useSignaturePad.spec.tsx
Problem: The new tests only cover canvas initialization. The core bug fix — pad.off() before resizing and pad.on() after, which resets _drawingStroke and prevents silent pointerdown drops — has zero coverage. A future refactor could silently regress the fix.
Fix: Add a test that captures the ResizeObserver callback and verifies call order:

it("calls pad.off(), resizes canvas, pad.redraw(), then pad.on() on resize", () => {
    let observerCallback: ResizeObserverCallback | undefined;
    (global.ResizeObserver as jest.Mock).mockImplementation((cb: ResizeObserverCallback) => {
        observerCallback = cb;
        return { observe: jest.fn(), disconnect: jest.fn() };
    });

    stubContainerDimensions(800, 400);
    const imageSource = buildImageSource(); // unavailable → pad initialises
    render(<TestHarness imageSource={imageSource} />);

    const padInstance = MockSignaturePad.mock.instances[0] as any;
    jest.clearAllMocks();

    act(() => {
        observerCallback!([], {} as ResizeObserver);
    });

    const offIdx  = (padInstance.off   as jest.Mock).mock.invocationCallOrder[0];
    const drawIdx = (padInstance.redraw as jest.Mock).mock.invocationCallOrder[0];
    const onIdx   = (padInstance.on    as jest.Mock).mock.invocationCallOrder[0];

    expect(offIdx).toBeLessThan(drawIdx);
    expect(drawIdx).toBeLessThan(onIdx);
});

⚠️ Low — Empty act(() => {}) in loading test adds no value

File: packages/pluggableWidgets/signature-web/src/__tests__/useSignaturePad.spec.tsx line 100
Note: act(() => {}) with no async work inside it flushes no pending state. It looks like it was added to "wait for effects" but actually does nothing in this synchronous context. Remove it to keep the test's intent clear.


⚠️ Low — SizeContainer now hardcodes signature-specific classes

File: packages/pluggableWidgets/signature-web/src/components/SizeContainer.tsx lines 54–60
Note: widget-signature-wrapper, form-control, mx-textarea-input, mx-textarea are baked into the inner div unconditionally. The component is private to this package so it's safe for now, but the name SizeContainer implies a general-purpose utility. If it's purely a signature component going forward, renaming it (e.g. SignatureContainer) would make the coupling explicit and prevent future misuse.


Positives

  • The pad.off() → resize → pad.redraw()pad.on() sequence is well-motivated in the PR description and the inline comment explains the _drawingStroke invariant clearly.
  • PEN_OPTIONS lookup table is a clean, type-safe replacement for the useMemo with if/else chains; exhaustiveness is guaranteed by Record<PenTypeEnum, Options>.
  • CHANGELOG entries are user-facing and behavior-focused — no implementation leakage.
  • Removing isSignatureInitialized ref is the right call: the signaturePadRef === null guard already encodes the same invariant with no extra bookkeeping.
  • The three-way nested if in the clear-on-reset effect is correctly collapsed to a single boolean expression.
  • Dimension-stubbing pattern (jest.spyOn(HTMLElement.prototype, "offsetWidth", "get")) is the right approach for jsdom layout constraints and is properly cleaned up in afterEach.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants